/*
 * Unidata Platform
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 *
 * Commercial License
 * This version of Unidata Platform is licensed commercially and is the appropriate option for the vast majority of use cases.
 *
 * Please see the Unidata Licensing page at: https://unidata-platform.com/license/
 * For clarification or additional options, please contact: info@unidata-platform.com
 * -------
 * Disclaimer:
 * -------
 * THIS SOFTWARE IS DISTRIBUTED "AS-IS" WITHOUT ANY WARRANTIES, CONDITIONS AND
 * REPRESENTATIONS WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE
 * IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, MERCHANTABLE QUALITY,
 * FITNESS FOR A PARTICULAR PURPOSE, DURABILITY, NON-INFRINGEMENT, PERFORMANCE AND
 * THOSE ARISING BY STATUTE OR FROM CUSTOM OR USAGE OF TRADE OR COURSE OF DEALING.
 */
package org.unidata.mdm.rest.data.service.impl;

import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.unidata.mdm.core.service.MetaModelService;
import org.unidata.mdm.core.type.model.AttributeElement;
import org.unidata.mdm.core.type.model.AttributeElement.AttributeValueType;
import org.unidata.mdm.core.type.model.EnumerationElement;
import org.unidata.mdm.core.type.model.LookupLinkElement;
import org.unidata.mdm.core.type.model.MeasurementCategoryElement;
import org.unidata.mdm.core.type.model.RelationElement;
import org.unidata.mdm.core.util.SecurityUtils;
import org.unidata.mdm.data.configuration.DataConfigurationConstants;
import org.unidata.mdm.meta.configuration.Descriptors;
import org.unidata.mdm.meta.service.LookupService;
import org.unidata.mdm.meta.type.search.EntityIndexType;
import org.unidata.mdm.meta.type.search.RecordHeaderField;
import org.unidata.mdm.meta.type.search.RelationHeaderField;
import org.unidata.mdm.rest.data.service.SearchResultHitModifier;
import org.unidata.mdm.rest.search.type.result.SearchResultProcessingElements;
import org.unidata.mdm.search.context.SearchRequestContext;
import org.unidata.mdm.search.dto.SearchResultDTO;
import org.unidata.mdm.search.dto.SearchResultExtendedValueDTO;
import org.unidata.mdm.search.dto.SearchResultHitDTO;
import org.unidata.mdm.search.dto.SearchResultHitFieldDTO;
import org.unidata.mdm.search.service.SearchService;
import org.unidata.mdm.search.type.form.FieldsGroup;
import org.unidata.mdm.search.type.form.FormField;
import org.unidata.mdm.search.type.query.SearchQuery;
import org.unidata.mdm.search.type.sort.SortField;
import org.unidata.mdm.search.util.SearchUtils;
import org.unidata.mdm.system.type.annotation.ConfigurationRef;
import org.unidata.mdm.system.type.configuration.ConfigurationValue;
import org.unidata.mdm.system.type.runtime.MeasurementPoint;
import org.unidata.mdm.system.util.TextUtils;

import static org.unidata.mdm.meta.type.search.RelationHeaderField.*;

@Component
public class SearchResultHitModifierImpl implements SearchResultHitModifier {

    private static final String DEFAULT_DISPLAY_VALUES_NOT_FOUND_MESSAGE_KEY = "app.data.cannot.resolve.display.values";

    private String displayValuesNotFoundMessage = null;

    @ConfigurationRef(DataConfigurationConstants.PROPERTY_INDEX_DATE_DISPLAY_FORMAT)
    private ConfigurationValue<DateTimeFormatter> dateDisplayFormatter;

    @ConfigurationRef(DataConfigurationConstants.PROPERTY_INDEX_TIME_DISPLAY_FORMAT)
    private ConfigurationValue<DateTimeFormatter> timeDisplayFormatter;

    @ConfigurationRef(DataConfigurationConstants.PROPERTY_INDEX_TIMESTAMP_DISPLAY_FORMAT)
    private ConfigurationValue<DateTimeFormatter> dateTimeDisplayFormatter;

    /**
     * Search service.
     */
    @Autowired
    private SearchService searchService;
    /**
     * Meta model service
     */
    @Autowired
    private MetaModelService metaModelService;

    // TODO @Modules
//    /**
//     * Nodes cache.
//     */
//    @Autowired
//    private ClassifiersMetaModelCacheComponent cacheComponent;

    @Autowired
    private LookupService lookupService;

    @Override
    public Map<String, List<Triple<String, Date, Date>>> getDisplayNamesForRelationTo(String relationName, List<Triple<String, Date, Date>> toIds) {
        RelationElement relationDef = metaModelService.instance(Descriptors.DATA).getRelation(relationName);
        Collection<String> displayAttributes = CollectionUtils.isNotEmpty(relationDef.getRightPresentation().getDisplayAttributes())
                ? relationDef.getRightPresentation().getDisplayAttributes()
                : relationDef.getRight().getMainDisplayableAttributes().keySet();

        return getDisplayNameForRecord(relationDef.getRight().getName(), relationDef.getRightPresentation().showAttributeNames(), displayAttributes, toIds);
    }

    private Map<String, List<Triple<String, Date, Date>>> getDisplayNameForRecord(String entityName, boolean useAttributeNamesForDisplay, Collection<String> displayFields, List<Triple<String, Date, Date>> toIds) {

        List<String> returnFields = new ArrayList<>(displayFields);
        returnFields.add(RecordHeaderField.FIELD_FROM.getName());
        returnFields.add(RecordHeaderField.FIELD_TO.getName());
        returnFields.add(RecordHeaderField.FIELD_ETALON_ID.getName());

        FieldsGroup dataIdsFilter = FieldsGroup.or();
        for (Triple<String, Date, Date> toId : toIds) {
            dataIdsFilter.add(FieldsGroup.and(
                    FormField.exact(RecordHeaderField.FIELD_ETALON_ID, toId.getLeft()),
                    FormField.range(RecordHeaderField.FIELD_FROM, null, toId.getRight()),
                    FormField.range(RecordHeaderField.FIELD_TO, toId.getMiddle(), null)));
        }

        FormField activeOnly = FormField.exact(RecordHeaderField.FIELD_INACTIVE, Boolean.FALSE);
        FormField notDeletedOnly = FormField.exact(RecordHeaderField.FIELD_DELETED, Boolean.FALSE);

        FieldsGroup toSideRestriction = FieldsGroup.and(activeOnly, notDeletedOnly);
        toSideRestriction.add(dataIdsFilter);

        SearchRequestContext ctx = SearchRequestContext.builder(EntityIndexType.RECORD, entityName, SecurityUtils.getCurrentUserStorageId())
                .totalCount(true)
                .fetchAll(false)
                .count(1000)
                .returnFields(returnFields)
                .query(SearchQuery.formQuery(toSideRestriction))
                .sorting(Collections.singletonList(SortField.of(RecordHeaderField.FIELD_FROM, SortField.SortOrder.ASC)))
                .build();

        SearchResultDTO toSideDTO = searchService.search(ctx);
        modifySearchResult(toSideDTO);

        Map<String, AttributeElement> attrsMap = metaModelService.instance(Descriptors.DATA).getElement(entityName).getAttributes();

        Map<String, List<Triple<String, Date, Date>>> displayNames = new HashMap<>();
        for(SearchResultHitDTO hit : toSideDTO.getHits()){
            displayNames.computeIfAbsent(hit.getId(), s -> new ArrayList<>())
                    .add(Triple.of(buildDisplayName(useAttributeNamesForDisplay, displayFields, attrsMap, hit),
                            SearchUtils.parseWithoutOffset(hit.getFieldFirstValue(RecordHeaderField.FIELD_FROM.getName())),
                            SearchUtils.parseWithoutOffset(hit.getFieldFirstValue(RecordHeaderField.FIELD_TO.getName()))));
        }
        return displayNames;
    }

    @Override
    public Map<String, List<Triple<String, Date, Date>>> getDisplayNamesForRelationTo(String relationName, @Nonnull  String fromEtalonId, String relationId, Date validFrom, Date validTo) {

        RelationElement relationDef = metaModelService.instance(Descriptors.DATA).getRelation(relationName);
        List<FormField> restrictions = new ArrayList<>();
        restrictions.add(FormField.exact(FIELD_FROM_ETALON_ID, fromEtalonId));
        restrictions.add(FormField.exact(FIELD_RELATION_NAME, relationDef.getName()));
        restrictions.add(FormField.exact(FIELD_DIRECTION_FROM, Boolean.FALSE));

        if(StringUtils.isNotEmpty(relationId)){
            restrictions.add(FormField.exact(RelationHeaderField.FIELD_ETALON_ID, relationId));
        }
        List<String> returnFields = Arrays.asList(FIELD_TO_ETALON_ID.getName(),
                RelationHeaderField.FIELD_FROM.getName(), RelationHeaderField.FIELD_TO.getName());

        SearchRequestContext rels = SearchRequestContext.builder(EntityIndexType.RELATION, relationDef.getRight().getName(), SecurityUtils.getCurrentUserStorageId())
                .query(SearchQuery.formQuery(FieldsGroup.and(restrictions)))
                .returnFields(returnFields)
                .count(1000)
                .build();

        SearchResultDTO resultDTO = searchService.search(rels);
        List<Triple<String, Date, Date>> dataIds = new ArrayList<>();
        for (SearchResultHitDTO hit : resultDTO.getHits()) {
            dataIds.add(Triple.of(hit.getFieldFirstValue(FIELD_TO_ETALON_ID.getName()),
                    SearchUtils.parse(hit.getFieldFirstValue(FIELD_FROM.getName())),
                    SearchUtils.parse(hit.getFieldFirstValue(FIELD_TO.getName()))));
        }

        return getDisplayNamesForRelationTo(relationDef.getName(), dataIds);
    }

    private String buildDisplayName(boolean useAttributeNameForDisplay, Collection<String> displayAttributes, Map<String, AttributeElement> attrsMap, SearchResultHitDTO hit) {
        List<String> arrtValues = new ArrayList<>();
        for (String attr : displayAttributes) {

            SearchResultHitFieldDTO hf = hit.getFieldValue(attr);

            // UN-7814
            if (hf != null && !hf.isEmpty()) {
                AttributeElement attrHolder = attrsMap.get(hf.getField());
                String converted;
                List<String> displayValues = hf.getDisplayValues();
                if (attrHolder != null && attrHolder.isArray()) {
                    converted = "[" + String.join(", ", displayValues) + "]";
                } else {
                    converted = String.valueOf(hf.isCollection()
                            ? displayValues.get(0) + " (" + String.join(", ", displayValues.subList(1, displayValues.size()).stream()
                            .filter(Objects::nonNull)
                            .map(Object::toString)
                            .collect(Collectors.toList())) + ")"
                            : displayValues.get(0));
                }

                if (useAttributeNameForDisplay) {
                    converted = attrHolder != null
                            ? attrHolder.getDisplayName() + ": " + converted
                            : converted;
                }

                arrtValues.add(converted);
            }
        }
        return String.join(StringUtils.SPACE, arrtValues);
    }

    @Override
    public String extractDisplayName(Map<String, List<Triple<String, Date, Date>>> displayNames, String etalonIdTo,
                                    Date validFrom, Date validTo, Date now) {
        String result = null;
        if (displayNames.containsKey(etalonIdTo)) {
            boolean calculateOnToday = (validFrom == null || !validFrom.after(now))
                    && (validTo == null || !validTo.before(now));

            Optional<Triple<String, Date, Date>> todayValue = Optional.empty();
            if (calculateOnToday) {
                todayValue = displayNames.get(etalonIdTo).stream()
                        // find intersection
                        .filter(r -> (r.getMiddle() == null || !r.getMiddle().after(now))
                                && (r.getRight() == null || !r.getRight().before(now)))
                        .findFirst();
            }

            if (todayValue.isPresent()) {
                result = todayValue.get().getLeft();
            } else {
                Optional<Triple<String, Date, Date>> leftDisplayValue =
                        displayNames.get(etalonIdTo).stream()
                                // find intersection
                                .filter(r -> (validTo == null || r.getMiddle() == null || !r.getMiddle().after(validTo))
                                        && ((validFrom == null) || r.getRight() == null || !r.getRight().before(validFrom)))
                                .findFirst();
                if (leftDisplayValue.isPresent()) {
                    result = leftDisplayValue.get().getLeft();
                }
            }
        }
        if (result == null) {
            if (displayValuesNotFoundMessage == null) {
                displayValuesNotFoundMessage = TextUtils.getText(DEFAULT_DISPLAY_VALUES_NOT_FOUND_MESSAGE_KEY);
            }
            result = displayValuesNotFoundMessage;
        }
        return result;
    }

    // TODO @Modules
//    private Map<? extends String, ? extends SearchResultHitFieldDTO> enrichValues(
//            final Map<String, SearchResultHitFieldDTO> fields
//    ) {
//        final Map<String, Map<String, Function<Collection<String>, List<String>>>> valueGeneratorsMap = fields.keySet().stream()
//                .map(this::classifierAndAttrPair)
//                .collect(Collectors.groupingBy(Pair::getKey)).entrySet().stream()
//                .map(e ->
//                        Pair.of(
//                                e.getKey(),
//                                e.getValue().stream()
//                                        .map(Pair::getValue)
//                                        .map(attrName -> Pair.of(attrName, /*buildValueGenerator(e.getKey(), attrName))*/)
//                                        .filter(p -> p.getValue() != null)
//                                        .collect(Collectors.toMap(Pair::getKey, Pair::getValue))
//                        )
//                )
//                .collect(Collectors.toMap(Pair::getKey, Pair::getValue));
//        fields.forEach((key, value) -> {
//            final Pair<String, String> classifierAndAttrPair = classifierAndAttrPair(key);
//            final String classifierName = classifierAndAttrPair.getLeft();
//            if (valueGeneratorsMap.containsKey(classifierName)) {
//                final Map<String, Function<Collection<String>, List<String>>> attrsMap = valueGeneratorsMap.get(classifierName);
//                final String attrName = classifierAndAttrPair.getRight();
//                if (attrsMap.containsKey(attrName)) {
//                    final Function<Collection<String>, List<String>> valueGenerator = attrsMap.get(attrName);
//                    fields.put(
//                            key,
//                            new SearchResultHitFieldDTO(
//                                    value.getField(),
//                                    valueGenerator.apply(
//                                            value.getValues().stream()
//                                                    .map(Object::toString)
//                                                    .collect(Collectors.toList())
//                                    ).stream()
//                                            .map(v -> (Object) v)
//                                            .collect(Collectors.toList())
//                            )
//                    );
//                }
//            }
//        });
//        return fields;
//    }

    private Pair<String, String> classifierAndAttrPair(String s) {
        return Pair.of(
                StringUtils.substringBefore(s, SearchUtils.DOT),
                StringUtils.substringAfter(s, SearchUtils.DOT)
        );
    }

    private final Function<String, Function<Collection<String>, List<String>>> toValueGenerator =
            name -> (Function<Collection<String>, List<String>>) values -> {
                if (CollectionUtils.isEmpty(values)) {
                    return Collections.emptyList();
                }

                return values.stream()
                        .map(v -> {
                            Pair<String, String> lookupDisplayValue = lookupService.getLookupDisplayNameById(name, v, null, null, null, false);
                            return lookupDisplayValue.getValue() == null ? v : lookupDisplayValue.getValue();
                        })
                        .collect(Collectors.toList());
            };

    // TODO @Modules
//    private Function<Collection<String>, List<String>> buildValueGenerator(String classifier, String attrName) {
//        final CachedClassifier clsf = cacheComponent.getClassifier(classifier);
//        final Optional<String> lookupName = clsf.getNodes().values().stream()
//                .flatMap(n -> n.getAttributes().values().stream().flatMap(Collection::stream))
//                .filter(a -> a.getName().equals(attrName))
//                .filter(a -> a instanceof CachedClassifierNodeLinkableAttribute)
//                .map(a -> (CachedClassifierNodeLinkableAttribute) a)
//                .filter(CachedClassifierNodeLinkableAttribute::isLookupLink)
//                .map(CachedClassifierNodeLinkableAttribute::getLookupName)
//                .findFirst();
//        return lookupName
//                .map(toValueGenerator)
//                .orElse(null);
//    }


    /**
     * Method provide search with after processing which modify all processed elements to display view.
     *
     * @param searchResult result of search by ctx
     */
    @Override
    public void modifySearchResult(@Nonnull SearchResultDTO searchResult) {
        innerModifySearchResult(searchResult, EnumSet.allOf(SearchResultProcessingElements.class), null, null);
    }

    /**
     * Method provide search with after processing which modify processed elements to display view.
     *
     * @param searchResult result of search by ctx
     * @param enumSet elements to process
     */
    @Override
    public void modifySearchResult(@Nonnull SearchResultDTO searchResult, EnumSet<SearchResultProcessingElements> enumSet) {
        innerModifySearchResult(searchResult, enumSet, null, null);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void modifySearchResult(SearchResultDTO searchResult, String attributesSource, String fieldPrefix) {
        innerModifySearchResult(searchResult, EnumSet.allOf(SearchResultProcessingElements.class), attributesSource, fieldPrefix);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void modifySearchResult(SearchResultDTO searchResult, String attributesSource, String fieldPrefix,
            Set<SearchResultProcessingElements> elements) {
        innerModifySearchResult(searchResult, elements, attributesSource, fieldPrefix);
    }

    private void innerModifySearchResult(
            @Nonnull SearchResultDTO searchResult,
            Set<SearchResultProcessingElements> processingElements,
            String attributesSourceName,
            String fieldPrefix) {

        MeasurementPoint.start();
        try {

            if (CollectionUtils.isEmpty(searchResult.getFields())) {
                return;
            }

            Map<String, AttributeElement> attributesMap = Objects.isNull(attributesSourceName)
                    ? metaModelService.instance(Descriptors.DATA).getElement(searchResult.getEntity()).getAttributes()
                    : metaModelService.instance(Descriptors.DATA).getElement(attributesSourceName).getAttributes();

            // Right is the displayableField, left is codeAttrName

            for (String displayableField : searchResult.getFields()) {

                if (isSystemDateAttribute(displayableField)) {
                    for (SearchResultHitDTO hit : searchResult.getHits()) {
                        SearchResultHitFieldDTO field = hit.getFieldValue(displayableField);
                        if (field == null || field.isNullField()) {
                            continue;
                        }
                        String parsedDate = SearchUtils.formatForUI(field.getFirstValue().toString());
                        field.setExtendedValues(Collections.singletonList(new SearchResultExtendedValueDTO(
                                parsedDate,
                                parsedDate)));
                    }
                }

                AttributeElement attr = attributesMap.get(Objects.nonNull(fieldPrefix) && displayableField.startsWith(fieldPrefix)
                        ? displayableField.substring(fieldPrefix.length())
                        : displayableField);

                if (Objects.isNull(attr)) {
                    continue;
                }

                // process links
                if (attr.isLookupLink() && processingElements.contains(SearchResultProcessingElements.LOOKUP)) {
                    for (SearchResultHitDTO hit : searchResult.getHits()) {
                        Date validFrom = SearchUtils.parse(hit.getFieldFirstValue(RecordHeaderField.FIELD_FROM.getName()));
                        Date validTo = SearchUtils.parse(hit.getFieldFirstValue(RecordHeaderField.FIELD_TO.getName()));
                        SearchResultHitFieldDTO field = hit.getFieldValue(displayableField);
                        if (field == null || field.isNullField()) {
                            continue;
                        }
                        List<SearchResultExtendedValueDTO> extendedValues = new ArrayList<>();
                        field.getValues().forEach(fieldValue -> {

                            LookupLinkElement lle = attr.getLookupLink();
                            Pair<String, String> lookupDisplayName = lookupService.getLookupDisplayNameById(
                                    lle.getLookupLinkName(), fieldValue, validFrom, validTo,
                                    lle.getPresentation().getDisplayAttributes(), lle.getPresentation().showAttributeNames());

                            if (lookupDisplayName != null) {
                                extendedValues.add(new SearchResultExtendedValueDTO(
                                        fieldValue,
                                        lookupDisplayName.getValue(),
                                        lookupDisplayName.getKey()));
                            }
                        });
                        field.setExtendedValues(extendedValues);
                    }
                }

                // process measured values!
                if (attr.isMeasured() && processingElements.contains(SearchResultProcessingElements.MEASURED)) {
                    addShortMeasurementUnitNameToHits(searchResult, displayableField, attr.getMeasured().getCategoryId());
                }

                // process enum values
                if (attr.isEnumValue() && processingElements.contains(SearchResultProcessingElements.ENUM)) {
                    for (SearchResultHitDTO hit : searchResult.getHits()) {

                        SearchResultHitFieldDTO field = hit.getFieldValue(displayableField);
                        if (field == null || field.isNullField()) {
                            continue;
                        }

                        String displayValue = getEnumDisplayValues(attr.getEnumName(), field.getValues());
                        // todo
                        field.setExtendedValues(Collections.singletonList(
                                new SearchResultExtendedValueDTO(field.getFirstValue(), displayValue)));
                    }
                }

                // process date values
                if (attr.isDate() && processingElements.contains(SearchResultProcessingElements.DATE)) {
                    for (SearchResultHitDTO hit : searchResult.getHits()) {

                        SearchResultHitFieldDTO field = hit.getFieldValue(displayableField);
                        if (field == null || field.isNullField()) {
                            continue;
                        }

                        String displayValue = getDateDisplayValues(attr.getValueType(), field.getFirstValue());
                        field.setExtendedValues(Collections.singletonList(
                                new SearchResultExtendedValueDTO(field.getFirstValue(), displayValue)));
                    }
                }
            }

          } finally {
            MeasurementPoint.stop();
        }
    }

    private void addShortMeasurementUnitNameToHits(@Nonnull SearchResultDTO searchResult,
                                                   @Nonnull String displayableField,
                                                   @Nonnull String categoryId) {

        MeasurementCategoryElement el = metaModelService.instance(Descriptors.MEASUREMENT_UNITS)
            .getCategory(categoryId);

        String shortName = el == null ? null : el.getBaseUnit().getDisplayName();

        if (shortName == null) {
            return;
        }

        for (SearchResultHitDTO hit : searchResult.getHits()) {

            SearchResultHitFieldDTO oldField = hit.getFieldValue(displayableField);
            if (oldField == null || oldField.isNullField()) {
                continue;
            }

            List<SearchResultExtendedValueDTO> withUnits = oldField.getValues()
                    .stream()
                    .filter(Objects::nonNull)
                    .map(value -> new SearchResultExtendedValueDTO(value, value.toString() + " " + shortName))
                    .collect(Collectors.toList());

            oldField.setExtendedValues(withUnits);

        }
    }


    @Nonnull
    private String getEnumDisplayValues(String enumName, List<Object> values) {

        if (CollectionUtils.isEmpty(values)) {
            return StringUtils.EMPTY;
        }

        EnumerationElement ew = metaModelService
                .instance(Descriptors.ENUMERATIONS)
                .getEnumeration(enumName);

        if (Objects.isNull(ew)) {
            return StringUtils.EMPTY;
        }

        List<String> displayValues = new ArrayList<>();
        for (Object v : values) {
            displayValues.add(ew.getEnumerationValue(v.toString()).getDisplayName());
        }

        return String.join(StringUtils.SPACE, displayValues);
    }

    private boolean isSystemDateAttribute(String fieldName) {
        return RecordHeaderField.FIELD_FROM.getName().equals(fieldName)
                || RecordHeaderField.FIELD_TO.getName().equals(fieldName)
                || RecordHeaderField.FIELD_CREATED_AT.getName().equals(fieldName)
                || RecordHeaderField.FIELD_UPDATED_AT.getName().equals(fieldName);
    }


    private String getDateDisplayValues(AttributeValueType dataType, Object value) {
        String result;
        switch (dataType) {
            case DATE:
                result = convertDate(value.toString(), DateTimeFormatter.ISO_LOCAL_DATE, dateDisplayFormatter.getValue());
                break;
            case TIME:
                result = convertDate(value.toString(), DateTimeFormatter.ISO_LOCAL_TIME, timeDisplayFormatter.getValue());
                break;
            case TIMESTAMP:
                result = convertDate(value.toString(), DateTimeFormatter.ISO_LOCAL_DATE_TIME, dateTimeDisplayFormatter.getValue());
                break;
            default:
                result = value.toString();
        }
        return result;
    }

    private static String convertDate(String d, DateTimeFormatter fromFormatter, DateTimeFormatter toFormatter) {
        return toFormatter.format(fromFormatter.parse(d));
    }

}
